#include <iostream>
#include <filesystem>

#define NK_IMPLEMENTATION
#define NK_SDL_GL3_IMPLEMENTATION
#include <interface.h>

#if defined(_WIN32) || defined(__CYGWIN__)
  #include <direct.h>
  void changePath(const char *path) {
    _chdir(path);
  }
#else
  #include <unistd.h>
  void changePath(const char *path) {
    chdir(path);
  }
#endif

sdl::Window::Wrapped window_;
gl::Context context_ = nullptr;
std::unique_ptr<nk::SDLContext> nkctx_ = nullptr;
std::vector<std::shared_ptr<IDrawable>> draws_;
std::shared_ptr<std::vector<std::function<std::shared_ptr<IDrawable>()>>> createDraws_ = nullptr;
extern void imgui(nk_context *ctx, struct nk_rect &rc);
void settingApply();
const char *title_ = "Learn OpenGL3.3";
std::filesystem::path binDir_;

void getBinDir(const char *arg0) {
  std::filesystem::path p(arg0);
  binDir_ = p.parent_path();
}

void adjustDrawIndex() {
  if (setting_.drawIndex < 0) setting_.drawIndex = 0;
  if (setting_.drawIndex >= draws_.size()) setting_.drawIndex = draws_.size() - 1;
}
std::shared_ptr<IDrawable> currentDrawable() {
  if (setting_.drawIndex < 0 || setting_.drawIndex >= draws_.size())
    return nullptr;
  return draws_.at(setting_.drawIndex);
}
std::shared_ptr<IDrawable> lastDrawable() {
  if (setting_.lastDrawIndex < 0 || setting_.lastDrawIndex >= draws_.size())
    return nullptr;
  return draws_.at(setting_.lastDrawIndex);
}
void changeTitle() {
  auto d = currentDrawable();
  if (!d) return;
  std::string title = title_;
  title += " - ";
  title += d->title();
  window_->setTitle(title.c_str());
}
void changeCurDir() {
  auto d = currentDrawable();
  if (!d) return;
  auto cd = binDir_;
  cd.append(d->resPath());
  auto path = cd.generic_string();
  changePath(path.c_str());
}
void changeDrawable(int newIdx) {
  auto idx = setting_.drawIndex;
  setting_.drawIndex = newIdx;
  adjustDrawIndex();
  if (setting_.drawIndex == setting_.lastDrawIndex) return;
  setting_.lastDrawIndex = idx;

  auto ld = lastDrawable();
  if (ld && ld->inited()) ld->douninit();

  changeCurDir();
  changeTitle();

  auto cd = currentDrawable();
  if (!cd) return;
  if (!cd->inited())
    cd->doinit();
}

void handleEvent(sdl::event::Event &evt) {
  if (evt.type == SDL_KEYDOWN) {
    if (evt.key.keysym.sym == SDLK_ESCAPE) {
      sdl::event::Event close;
      close.type = SDL_QUIT;
      sdl::event::push(&close);
    }
  } else if (evt.type == SDL_WINDOWEVENT) {
    if (evt.window.event == SDL_WINDOWEVENT_RESIZED)
      glViewport(0, 0, evt.window.data1, evt.window.data2);
  }

  auto d = currentDrawable();
  if (!d) return;
  d->processInput(evt);
}
void update(float elapsedTime) {
  auto d = currentDrawable();
  if (d) d->update(elapsedTime);
}
void render() {
  int width, height;
  window_->getSize(&width, &height);
  struct nk_rect rc = { 0.f, 0.f, (float)width, (float)height };

  glClearColor(setting_.bkcolor.r, setting_.bkcolor.g, setting_.bkcolor.b, setting_.bkcolor.a);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  auto d = currentDrawable();
  if (d) {
    d->render();
    d->imgui(nkctx_->get(), rc);
  }
  imgui(nkctx_->get(), rc);
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  nkctx_->render(NK_ANTI_ALIASING_ON, MAX_VERTEX_MEMORY, MAX_ELEMENT_MEMORY);
  gl::swapWindow(window_);
}

void addCreateDrawable(std::function<std::shared_ptr<IDrawable>()> fun) {
  if (!fun) return;
  if (!createDraws_) createDraws_ = std::make_shared<std::vector<std::function<std::shared_ptr<IDrawable>()>>>();
  createDraws_->emplace_back(fun);
}
void createDrawables() {
  for (auto &v : *createDraws_) {
    if (!v) continue;
    auto d = v();
    if (!d) continue;
    draws_.emplace_back(d);
  }
  std::sort(draws_.begin(), draws_.end()
      , [](const std::shared_ptr<IDrawable> &a, const std::shared_ptr<IDrawable> &b) -> bool {
    return a->index() < b->index();
  });
}
void destroyDrawables() {
  for (auto &v : draws_) {
    if (!v || !v->inited()) continue;
    v->douninit();
  }
  draws_.clear();
}

int main(int argc, char *argv[]) {
  getBinDir(argv[0]);

  gl::setAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
  gl::setAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
  gl::setAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);

  auto rst = sdl::init(SDL_INIT_EVERYTHING);
  if (sdl::isFailed(rst)) {
    std::cerr << "ERROR::init." << std::endl;
    return -1;
  }

  bool bok = false;
  window_ = sdl::Window::create(title_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED
      , 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
  if (window_) context_ = gl::createContext(window_);
  if (context_) bok = gladLoadGLLoader((GLADloadproc)gl::getProcAddress);
  if (bok) nkctx_ = std::make_unique<nk::SDLContext>(window_);
  if (nkctx_) {
    struct nk_font_atlas *atlas;
    nkctx_->font_stash_begin(&atlas);
    nkctx_->font_stash_end();
  }

  if (!bok || !context_ || !nkctx_ || !window_) {
    if (nkctx_) nkctx_->shutdown();
    if (context_) gl::deleteContext(context_);
    if (window_) window_->destroy();
    sdl::quit();
    std::cerr << "ERROR::create." << std::endl;
    return 0;
  }

  glEnable(GL_DEPTH_TEST);
  
  gl::makeCurrent(window_, context_);

  createDrawables();
  changeDrawable(0);

  bool quit = false;
  sdl::event::Event evt;
  Uint32 lastTime = 0, curTime = 0;
  float elapsedTime = 0.f;
  while (!quit) {
    nk_input_begin(nkctx_->get());
    while (sdl::event::poll(&evt)) {
      if (evt.type == SDL_QUIT) {
        quit = true;
      }
      nkctx_->handle_event(&evt);
      handleEvent(evt);
    }
    nk_input_end(nkctx_->get());

    curTime = sdl::timer::getTicks();
    elapsedTime = (curTime - lastTime) / 1000.f;
    lastTime = curTime;
    update(elapsedTime);
    render();
    settingApply();
  }

  destroyDrawables();
  nkctx_->shutdown();
  gl::deleteContext(context_);
  window_->destroy();

  sdl::quit();
  return 0;
}
